package heartbeat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

import sdm.repackaged.org.json.JSONArray;
import sdm.repackaged.org.json.JSONException;
import sdm.repackaged.org.json.JSONObject;

public class TcpClient
{
	private PCallback<MessageCBType> mOnAuthenticationCallback;
	private PCallback<MessageCBType> mOnMessageCallback;
	private PCallback<TcpClient> mOnClientDisconnectCallback;
	private PCallback<ChannelsCBType> mOnChannelsReceivedCallback;
	private Queue<ChannelMessagePair> mChannelMessageQueue;
	private Map<String, Integer> mChannelMap;
	private static final int LIBRARY_CHANNEL_SUBSCRIPTION = 3;
	private TcpConnection mConnection;
	private boolean mClosed = false;
	public TcpClient(String address, int port, boolean ssl) throws IOException, Exception
	{
		if(ssl)
			mConnection = new SslTcpConnection(address, port);
		else
			mConnection = new TcpConnection(address, port);
		
		mChannelMessageQueue = new LinkedList<ChannelMessagePair>();
	}
	
	//This needs to be synchronized since the channel map can be used from both threads.
	protected synchronized String channelName(int channelNumber)
	{
		if(!channelMapValid())
			return null;
		
		//Go through the map to try and find the String that maps to this integer.
		for(Map.Entry<String, Integer> entry : mChannelMap.entrySet())
		{
			if(entry.getValue() != channelNumber)
				continue;
			
			return entry.getKey();
		}
		
		return null;
	}
	
	protected synchronized Integer channelNumber(String channelName)
	{
		if(!channelMapValid())
			return null;
		
		if(!mChannelMap.containsKey(channelName))
			return null;
		
		return mChannelMap.get(channelName);
	}
	
	public void close()
	{
		if(mClosed)
			return;

		mConnection.close();
		mClosed = true;
		invokeOnClientDisconnectCallback();
	}
	
	protected void addChannelMessageToQueue(String pChannelName, JSONObject pMessage)
	{
		mChannelMessageQueue.add(new ChannelMessagePair(pChannelName, pMessage));
	}
	
	//This needs to be synchronized since the channel map can be used from both threads.
	protected synchronized boolean channelMapValid()
	{
		return mChannelMap != null;
	}
	
	protected void emptyChannelMessageQueue() throws IOException
	{
		while(!mChannelMessageQueue.isEmpty())
		{
			ChannelMessagePair pChannelMessagePair = mChannelMessageQueue.poll();
			write(pChannelMessagePair.channelName(), pChannelMessagePair.message());
		}
	}
	
	//This needs to be synchronized since the channel map can be used from both threads.
	protected synchronized void fillChannelMap(JSONObject pChannelList)
	{
		mChannelMap = new HashMap<String, Integer>();
		try
		{
			JSONArray channels = pChannelList.getJSONArray("channels");
			for(int i = 0; i < channels.length(); ++i)
			{
				JSONObject channel = channels.getJSONObject(i);
				String channelName = channel.names().getString(0);
				mChannelMap.put(channelName, channel.getInt(channelName));
			}
		}
		//Malformed channel list, Just return.
		catch(JSONException e)
		{
			return;
		}
	}
	
	public synchronized void invokeOnChannelsReceivedCallback()
	{
		if(mOnChannelsReceivedCallback == null)
			return;
		
		if(mChannelMap == null)
			return;
		
		List<String> pChannelList = new ArrayList<String>();
		for(String channel : mChannelMap.keySet())
			pChannelList.add(channel);
		
		ChannelsCBType cb = new ChannelsCBType(this, pChannelList);
		
		mOnChannelsReceivedCallback.invoke(cb);
	}
	
	public void invokeOnAuthenticationCallback(MessageCBType message)
	{
		if(mOnAuthenticationCallback == null)
			return;
		
		mOnAuthenticationCallback.invoke(message);
	}
	
	public void invokeOnClientDisconnectCallback()
	{
		if(mOnClientDisconnectCallback == null)
			return;
		
		mOnClientDisconnectCallback.invoke(this);
	}
	
	public void invokeOnMessageCallback(MessageCBType message)
	{
		if(mOnMessageCallback == null)
			return;
		
		mOnMessageCallback.invoke(message);
	}
	
	public Packet read() throws IOException
	{
		return mConnection.read();
	}
	
	protected void setOnAuthenticationCallback(PCallback<MessageCBType> pCallback)
	{
		mOnAuthenticationCallback = pCallback;
	}
	
	protected void setOnChannelsReceivedCallback(PCallback<ChannelsCBType> pCallback)
	{
		mOnChannelsReceivedCallback = pCallback;
	}
	
	protected void setOnClientDisconnectCallback(PCallback<TcpClient> pCallback)
	{
		mOnClientDisconnectCallback = pCallback;
	}
	
	protected void setOnMessageCallback(PCallback<MessageCBType> pCallback) 
	{
		mOnMessageCallback = pCallback;
	}
	
	protected synchronized void subscribeTo(List<String> channelNames) throws IOException
	{
		JSONObject outMessage = new JSONObject();
		JSONArray channelListJson = new JSONArray();
		for(String cName : channelNames)
		{
			if(!mChannelMap.containsKey(cName))
				continue;
			
			int channelNumber = mChannelMap.get(cName);
			channelListJson.put(channelNumber);
		}
		
		try 
		{
			outMessage.put("channels", channelListJson);
		} 
		catch (JSONException e) 
		{
			return;
		}
		
		write(new Packet(LIBRARY_CHANNEL_SUBSCRIPTION, outMessage));
	}
	
	
	protected void write(Packet msg) throws IOException
	{
		mConnection.write(msg);
	}
	
	protected synchronized void write(String channelName, JSONObject message) throws IOException
	{
		if(!channelMapValid())
			return;
		
		Integer channelNumber;
		if((channelNumber = mChannelMap.get(channelName)) == null)
			return;
		
		Packet pPacket = new Packet(channelNumber, message);
		write(pPacket);
	}
}
